//    Copyright (c) 2009 Elwyn Malethan
//
//    This file is part of SeemoreJ.
//
//    SeemoreJ is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    (at your option) any later version.
//
//    SeemoreJ is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU General Public License for more details.

//    You should have received a copy of the GNU General Public License
//    along with SeemoreJ.  If not, see <http://www.gnu.org/licenses/>.

package com.malethan.seemorej.context.impl;

import com.malethan.seemorej.Flash;
import com.malethan.seemorej.SeemoreJ;
import com.malethan.seemorej.context.Context;
import com.malethan.seemorej.rendering.Rendering;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>Copyright &copy; 2008 Elwyn Malethan</p>
 */
public class ContextImpl implements Context {
    //----------------------------------------------------------------------- Static Properties and Constants

    private static final Log log = LogFactory.getLog(ContextImpl.class);

    //----------------------------------------------------------------------- Static Methods

    private static void setProperty(Object bean, String propertyName, String paramValue) {
        try {
            Field field = bean.getClass().getDeclaredField(propertyName);
            if(field.getType().isAssignableFrom(Date.class)) {
                setDateProperty(bean, propertyName, paramValue);
            } else {
                BeanUtils.setProperty(bean, propertyName, paramValue);
            }
        } catch (IllegalAccessException e) {
            log.warn(e);
        } catch (InvocationTargetException e) {
            log.warn(e);
        } catch (NoSuchFieldException e) {
            log.debug(e);
        }
    }

    private static void setDateProperty(Object bean, String propertyName, String paramValue) throws IllegalAccessException, InvocationTargetException {
        Date sqlDate;
        try {
            sqlDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(paramValue);
            BeanUtils.setProperty(bean, propertyName, sqlDate);
        } catch (ParseException e) {
            log.warn(e);
            try {
                sqlDate = new SimpleDateFormat("yyyy-MM-dd").parse(paramValue);
                BeanUtils.setProperty(bean, propertyName, sqlDate);
            } catch (ParseException e1) {
                log.warn(e1);
            }
        }
    }

    //----------------------------------------------------------------------- Instance Properties

    private HttpServletRequest request;
    private HttpServletResponse response;
    private HttpSession session;
    private Rendering rendering;
    private boolean rendered;
    private String controller;
    private String action;
    private String format;

    //----------------------------------------------------------------------- Constructors

    ContextImpl(HttpServletRequest request, HttpServletResponse response) {
        this.request = request;
        this.response = response;
        this.session = this.request.getSession();
        this.rendering = null;
        this.rendered = false;

        setController(request.getParameter(SeemoreJ.CONTROLLER));
        setAction(request.getParameter(SeemoreJ.ACTION));
        setFormat(request.getParameter(SeemoreJ.FORMAT));
    }

    //----------------------------------------------------------------------- Getters and Setters

    public Rendering getRendering() {
        return rendering;
    }

    public void setRendering(Rendering rendering) {
        if(rendered) {
            String msg = "Render has already been called, you may not call it more than once";
            throw new IllegalStateException(msg);
        }
        this.rendered = true;
        this.rendering = rendering;
    }

    public boolean isRendered() {
        return rendered;
    }

    public String getController() {
        return controller;
    }

    public void setController(String controller) {
        this.controller = controller;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    public void populateBean(String prefix, Object bean) {
        Enumeration paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = (String) paramNames.nextElement();
            Pattern paramPattern = Pattern.compile("^" + (prefix != null ? prefix + "\\." : "") + "(\\w[\\w\\d_]*)$");
            Matcher paramMatcher = paramPattern.matcher(paramName);
            String paramValue = request.getParameter(paramName);
            if(paramMatcher.matches()) {
                String propertyName = paramMatcher.group(1);
                setProperty(bean, propertyName, paramValue);
            }
        }
        populateMultipartDates(prefix, bean);
    }

    /**
     * TODO: Clean up this abortion of a method 
     */
    private void populateMultipartDates(String prefix, Object bean) {
        String paramPrefix = prefix != null ? prefix + "." : "";
        Map<String, String[]> params = request.getParameterMap();
        Field[] fields = bean.getClass().getDeclaredFields();
        for (Field field : fields) {
            if(field.getType().isAssignableFrom(Date.class)) {
                Map<Integer, String> dateFields = getDateTimeFieldWithSuffixes();

                GregorianCalendar gc = new GregorianCalendar();
                gc.set(Calendar.HOUR_OF_DAY, 0);
                gc.set(Calendar.MINUTE, 0);
                gc.set(Calendar.SECOND, 0);
                gc.set(Calendar.MILLISECOND, 0);
                boolean haveSomeFields = false;
                for (Integer dField : dateFields.keySet()) {
                    String fieldParamName = paramPrefix + field.getName() + dateFields.get(dField);
                    if (params.containsKey(fieldParamName) && params.get(fieldParamName).length > 0) {
                        haveSomeFields = true;
                        Integer dFieldValue = Integer.valueOf(params.get(fieldParamName)[0]);
                        if(dField == Calendar.MONTH) {
                            dFieldValue -= 1;
                        }
                        log.debug("Setting " + field.getName() + dateFields.get(dField) + " with " + dFieldValue);
                        gc.set(dField, dFieldValue);
                    }
                }
                if (haveSomeFields) {
                    try {
                        BeanUtils.setProperty(bean, field.getName(), gc.getTime());
                    } catch (IllegalAccessException e) {
                        log.warn(e);
                    } catch (InvocationTargetException e) {
                        log.warn(e);
                    }
                }
            }
        }
    }

    private static Map<Integer, String> getDateTimeFieldWithSuffixes() {
        Map<Integer, String> dateFields = new HashMap<Integer, String>();
        dateFields.put(Calendar.YEAR, "_year");
        dateFields.put(Calendar.MONTH, "_month");
        dateFields.put(Calendar.DATE, "_day");

        dateFields.put(Calendar.HOUR_OF_DAY, "_hour");
        dateFields.put(Calendar.MINUTE, "_minute");
        dateFields.put(Calendar.SECOND, "_second");
        return dateFields;
    }

    public void populateBean(Object bean) {
        populateBean(null, bean);
    }

    public String getFormat() {
        return this.format;
    }

    public void setFormat(String format) {
        this.format = format;
    }

    //----------------------------------------------------------------------- Instance Methods

    public HttpServletRequest getRequest() {
        return request;
    }

    public HttpServletResponse getResponse() {
        return response;
    }

    public String flash(Flash type) {
        initialiseFlashIfNull();
        return getAndClearFlashValue(type);
    }

    public void flash(Flash type, String value) {
        initialiseFlashIfNull();
        Map<String, String> flashMap = (Map<String, String>) session.getAttribute(Flash.FLASH_KEY);
        flashMap.put(type.name(), value);
    }

    private String getAndClearFlashValue(Flash type) {
        Map<String, String> flashMap = (Map<String, String>) session.getAttribute(Flash.FLASH_KEY);
        String message = flashMap.get(type.name());
        flashMap.remove(type.name());
        return message;
    }

    private void initialiseFlashIfNull() {
        Object flash = session.getAttribute(Flash.FLASH_KEY);
        if (flash == null || !(flash instanceof Map)) {
            session.setAttribute(Flash.FLASH_KEY, new HashMap<String, String>());
        }
    }
}
